import { Web3 } from 'web3';

import { store } from '../hooks/store.ts';
import { convertIpfsToHttp } from '../pages/Main/utils.ts';

import { brokerABI, erc721Abi } from './contractsABI/ABI.ts';
import { p2pContractABI } from './contractsABI/p2pContractABI.ts';
import { getNFTMetadata, getTokenCollectionsByAccount } from './services.ts';

import type { TransferEvent } from './contractsABI/ABI.ts';
import type {
    MetadataList,
    NFTDetails,
    Offer,
    OfferWithNFTs,
    DataForSetP2P,
    TokenInfo, DataForGetP2P,
} from '../types.ts';
import type { ContractEvents } from 'web3-types';

export const brokerContractId = '0xf38B6Ce2E5eD2Cf8CeEaeAB060D1bDC46473608f'.trim();

export const p2pDataContractId = '0xF5dAdacDa128D3262AA215A3CE27A395A13A9ABC'.trim();

export const getOutgoingOffers = async () => {
    const web3 = new Web3(store.value().provider);
    const web3Contract = new web3.eth.Contract(brokerABI, brokerContractId);

    try {
        const account = store.value().account;
        const offers: Offer[] = await web3Contract.methods.getOffers().call();

        return offers.filter(offer => {
            return (offer.initiator.account.toUpperCase() === account && !offer.executed);
        });
    } catch (e){
        console.error(e);
        return [];
    }
};

export const getIncomeOffers = async () => {
    const web3 = new Web3(store.value().provider);
    const web3Contract = new web3.eth.Contract(brokerABI, brokerContractId);
    
    try {
        const account = store.value().account;
        const offers: Offer[] = await web3Contract.methods.getOffers().call();
        return offers.filter(offer => {
            return (offer.recipient.account.toUpperCase() === account && !offer.executed);
        });
    } catch (e){
        console.error(e);
        return [];
    }
};

export const  getERC721Tokens = async (
    { walletAddress, tokenContracts }: {walletAddress: string; tokenContracts: string[]},
) => {
    const web3 = new Web3(store.value().provider);
    const tokens: TokenInfo[] = [];

    for (const tokenAddress of tokenContracts) {
        const tokenContract = new web3.eth.Contract(erc721Abi, tokenAddress);

        const events = await tokenContract.getPastEvents('Transfer' as keyof ContractEvents<typeof erc721Abi> , {
            filter: { to: walletAddress },
            fromBlock: 0,
            toBlock: 'latest',
        }) as TransferEvent[];

        const tokensSet = new Set<bigint>();
        
        for (const event of events) {
            const tokenID = event.returnValues.tokenId;

            if (!tokensSet.has(tokenID)) {
                tokensSet.add(tokenID);
                const owner: string = await tokenContract.methods.ownerOf(tokenID).call();
                if (owner.toLowerCase() === walletAddress.toLowerCase()) {
                    const tokenURI: string = await tokenContract.methods.tokenURI(tokenID).call();
                    tokens.push({ tokenURI, tokenID, tokenContract: tokenAddress });
                }
            }
        }
    }

    return tokens;
};

export const getTokensByAccount = async (account: string) => {
    const tokenContracts = await getTokenCollectionsByAccount(account);
    return await getERC721Tokens({ walletAddress: account, tokenContracts });
};

export const getTokensMeta = async (tokens:  TokenInfo[]) => {
    const NFTsMetadata: MetadataList = [];

    const metaInfoArray = tokens.map((token => {
        return new Promise<void>((resolve) => {
            const metadata = {
                img: 'https://storage.yandexcloud.net/picture/error_no_picture.webp',
                name: 'нет имени',
                contract: token.tokenContract,
                checked: false,
                tokenId: token.tokenID,
            };

            getNFTMetadata(token.tokenURI)
                .then((response) => {
                    metadata.img = convertIpfsToHttp(response.image);
                    metadata.name = response.name;
                    resolve();
                })
                .catch(/*empty*/)
                .finally(() => {
                    NFTsMetadata.push(metadata);
                    resolve();
                });
        });
    }));
    await Promise.all(metaInfoArray);

    return NFTsMetadata;
};

export const getMetaOfTokensByAccount = async (account: string) => {
    const tokens = await getTokensByAccount(account);

    return await getTokensMeta(tokens);
};

export const createOffer = async ({ initiator, address, listOfNftInitiator, listOfNftRecipient }: OfferWithNFTs) => {
    const web3 = new Web3(store.value().provider);
    const web3Contract = new web3.eth.Contract(brokerABI, brokerContractId);
    
    const offerList = JSON.stringify(listOfNftRecipient.map(NFT => ({ [NFT.contract]: NFT.tokenId.toString() })));

    const gasEstimateForOffer = await web3Contract.methods.createOffer(address, offerList).estimateGas({ from: initiator });

    await web3Contract.methods.createOffer(address, offerList).send({ from: initiator, gas: gasEstimateForOffer.toString() })
        .on('receipt', receipt => {
            console.log('Квитанция о транзакции:', receipt);
        })
        .on('error', error => {
            console.error('Ошибка:', error);
        });

    const offers = await web3Contract.methods.getOffers().call();

    // @ts-expect-error -- to do
    const offerId = offers[offers.length - 1][0];

    let lastContract = '';

    for (const nft of listOfNftInitiator) {
        if (nft.contract !== lastContract) {
            if (nft.checked) {
                const nftContract = new web3.eth.Contract(erc721Abi, nft.contract);
                const isApproved = await nftContract.methods.isApprovedForAll(initiator, brokerContractId).call();

                if (!isApproved) {
                    await nftContract.methods.setApprovalForAll(brokerContractId, true).send({ from: initiator });
                    lastContract = nft.contract;
                }
            }
        }
    }

    const NFTForRecipient = listOfNftInitiator.reduce(
        (result: Array<{nftAddr: string; tokenID: bigint}>, NFT) => {
            if (NFT.checked) {
                result.push({
                    nftAddr: NFT.contract.trim(),
                    tokenID: NFT.tokenId,
                });
            }
            return result;
        }, []);

    try {
        const gasEstimateTransfer = await web3Contract.methods.confirm(NFTForRecipient, offerId).estimateGas({ from: initiator });
        await web3Contract.methods.confirm(NFTForRecipient,offerId).send({ from: initiator, gas: gasEstimateTransfer.toString() });

    } catch (e) {
        console.trace(e);
    }

};

export const getTokensMetaByContractAndId = async (initiatorTokens: NFTDetails[]) => {
    const web3 = new Web3(store.value().provider);
    const tokens: TokenInfo[] = [];
    for (const initiatorToken of initiatorTokens) {
        try {
            const tokenContract = new web3.eth.Contract(erc721Abi, initiatorToken.nftAddr);
            const tokenURI: string = await tokenContract.methods.tokenURI(initiatorToken.tokenID).call();
            tokens.push({
                tokenURI,
                tokenContract: initiatorToken.nftAddr,
                tokenID: initiatorToken.tokenID,
            });
        } catch (e) {
            console.error('TRY TO CHECK TOKENS DATA', initiatorToken, e);
        }
    }

    return await getTokensMeta(tokens);
};

export const rejectOffer = async (offerId: bigint) => {
    const web3 = new Web3(store.value().provider);
    const web3Contract = new web3.eth.Contract(brokerABI, brokerContractId);

    const account = store.value().account;
    
    const gasEstimateForOffer = await web3Contract.methods.reject(offerId).estimateGas({ from: account });
    await web3Contract.methods.reject(offerId).send({ from: account, gas: gasEstimateForOffer.toString() })
        .on('receipt', receipt => {
            console.log('Квитанция о транзакции:', receipt);
        })
        .on('error', error => {
            console.error('Ошибка:', error);
        });
};

export const confirmOffer = async ({ NFTs, offerId }: { NFTs: NFTDetails[], offerId: bigint }) => {
    const web3 = new Web3(store.value().provider);
    const web3Contract = new web3.eth.Contract(brokerABI, brokerContractId);

    const account = store.value().account;
    const gasEstimateForOffer = await web3Contract.methods.confirm(NFTs, offerId).estimateGas({ from: account });
    await web3Contract.methods.confirm(NFTs, offerId).send({ from: account, gas: gasEstimateForOffer.toString() })
        .on('receipt', receipt => {
            console.log('Квитанция о транзакции:', receipt);
        })
        .on('error', error => {
            console.error('Ошибка:', error);
        });
};

export const setP2PConnectionData = async (data: DataForSetP2P) => {
    const web3 = new Web3(store.value().provider);
    const p2pDataContract = new web3.eth.Contract(p2pContractABI, p2pDataContractId);
    
    const account = store.value().account;

    const gasEstimate = await p2pDataContract.methods.setData(data).estimateGas({ from: account });
    await p2pDataContract.methods.setData(data).send({ from: account, gas: gasEstimate.toString() });
};

export const getP2PConnectionData = async (data: DataForGetP2P): Promise<undefined | { walletConnectionOffer: string; walletICECandidates: string; }> => {
    const web3 = new Web3(store.value().provider);
    const p2pDataContract = new web3.eth.Contract(p2pContractABI, p2pDataContractId);
    
    return await p2pDataContract.methods.getData(data).call({ from: data.walletInitiator });
};